/*
 *  Copyright (C) 2011 Jaime Pavlich-Mariscal
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package cl.ucn.disc.biblio.refcluster;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.PrintWriter;
import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.TreeSet;

import org.apache.log4j.Logger;

public class ReferenceClusterer {

    private Clusterer clusterer = new Clusterer();
    public static final Logger LOGGER = Logger.getLogger(ReferenceClusterer.class.getName());
    ReferenceManager rm = new ReferenceManager();
    private final Comparator<Set<?>> setFirstElementComparator = new Comparator<Set<?>>() {

        @Override
        public int compare(Set<?> s1, Set<?> s2) {
            if (s1 != null && s2 != null) {
                Object obj1 = s1.iterator().next();
                Object obj2 = s2.iterator().next();
                if (obj1 != null && obj2 != null) {
                    return ((Comparable) obj1).compareTo(obj2); // it may throw ClassCastException
                }
            }
            return 0;

        }
    };
    private Matcher<Reference> journalVolPageMatcher = new Matcher<Reference>() {

        public boolean matches(String str1, String str2) {
            if (str1 == null || str2 == null) {
                return true;
            } else {
                return str1.equals(str2) || str1.startsWith(str2) || str2.startsWith(str1);
            }
        }

        @Override
        public boolean matches(Reference r1, Reference r2) {
            return r1.journalSentence.matches(r2.journalSentence) && matches(r1.getVolume(), r2.getVolume()) && matches(r1.getPage(), r2.getPage());
        }
    };
    Matcher<Sentence> sentenceMatcher = new Matcher<Sentence>() {

        @Override
        public boolean matches(Sentence s1, Sentence s2) {
            return s1.matches(s2);
        }
    };

    /**
     * @param args
     * @throws FileNotFoundException
     * @throws IOException
     * @throws NoSuchFieldException
     * @throws SecurityException
     * @throws IllegalAccessException
     * @throws IllegalArgumentException
     */
    public static void main(String[] args) throws FileNotFoundException {
        ReferenceClusterer rc = new ReferenceClusterer();
        rc.createClusterFileFromListFile(new File(args[0]), new File("out.txt"));

    }

    // FIXME Not tested yet
    public void createClusterFileFromWoSDB(File wosDBFolder, File clusterFile) throws FileNotFoundException, IOException {
        LOGGER.info("BEGIN create cluster file from DB");
        Set<Reference> refs = rm.readRefsFromWoSFolder(wosDBFolder);
        LOGGER.info("References read from " + wosDBFolder.getCanonicalPath());
        Collection<Set<Reference>> clusters = clusterReferences(refs);
        LOGGER.info("References clustered...");
        writeClusters(clusters, new PrintWriter(clusterFile));
        LOGGER.info("Clusters written to " + wosDBFolder.getCanonicalPath());


    }

    public void createClusterFileFromListFile(File refsFile, File clusterFile) throws FileNotFoundException {
        List<Reference> allRefs = rm.readRefsFromListFile(refsFile);
        Collection<Set<Reference>> clusters = clusterReferences(allRefs);
        writeClusters(clusters, new PrintWriter(clusterFile));
    }

    public void writeClusters(Collection<Set<Reference>> clusters, PrintWriter out) {
        LOGGER.info("Writing clusters to file");
//		PrintWriter w = new PrintWriter(clusterFile);


        // Orders clusters
        TreeSet<Set<Reference>> orderedClusters = new TreeSet<Set<Reference>>(new Comparator<Set<Reference>>() {

            @Override
            public int compare(Set<Reference> s1, Set<Reference> s2) {
                if (s1 == s2 || (s1 != null && s2 != null && s1.isEmpty() && s2.isEmpty())) {
                    return 0;
                }

                if (s1 == null) {
                    return -1;
                }
                if (s2 == null) {
                    return 1;
                }
                Reference r1 = rm.createSuperReference(s1);
                Reference r2 = rm.createSuperReference(s2);
                String str1 = r1.getAuthor() + r1.getYear() + r1.getJournal();
                String str2 = r2.getAuthor() + r2.getYear() + r2.getJournal();
                return str1.compareTo(str2);
            }
        });


        for (Set<Reference> refs : clusters) {
            orderedClusters.add(refs);

        }

        for (Set<Reference> refs : orderedClusters) {
            // Reverse-order references
            Set<Reference> orderedRefs = new TreeSet<Reference>();
            orderedRefs.addAll(refs);
            for (Reference r : orderedRefs) {
                out.println(r.originalString);
            }
            out.println();
        }
        out.flush();
        LOGGER.info("DONE");
    }

    public Set<Set<Reference>> readClusters(File clusterFile) throws FileNotFoundException {
        LOGGER.info("Opening cluster file " + clusterFile);

        Set<Set<Reference>> clusters = new TreeSet<Set<Reference>>(setFirstElementComparator);
        Scanner s = new Scanner(clusterFile);

        Set<Reference> cluster = new HashSet<Reference>();
        while (s.hasNextLine()) {
            try {
                Reference ref = new Reference(s.nextLine());
                cluster.add(ref);
            } catch (InvalidReferenceException e) {
                // If it can't read a reference, it assumes that it is the end of the cluster
                if (s.hasNextLine()) {
                	clusters.add(cluster);
                    cluster = new HashSet<Reference>();
                    
                }


            }
        }
        if (cluster.size() > 0) {
        	clusters.add(cluster);
        }
        s.close();
        LOGGER.info("DONE");

        return clusters;
    }

    public Set<Set<Reference>> clusterReferences(Collection<Reference> allRefs) {
        LOGGER.info("Processing references per year");
        Set<Set<Reference>> allRefEquivalenceClasses = new TreeSet<Set<Reference>>(setFirstElementComparator);
        OrderedMultiMap<String, Reference> refsPerYear = getRefsPerYear(allRefs);

        for (String year : refsPerYear.keySet()) {
            if (year == null) {
                continue;
            }
            LOGGER.info("Year: " + year);
            // Finds equivalent authors. The classes of equivalence correspond
            // to cliques in a graph where
            // vertexes are the authors and edges represents matching between
            // two sentences
            List<Sentence> authors;
            try {
                authors = getSentenceListFromField(refsPerYear.get(year), Reference.class.getDeclaredField("author"), null);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }

            Collection<Set<Sentence>> authorEquivalenceClasses = clusterer.findEquivalenceClasses(authors, sentenceMatcher);

            MultiMap<Sentence, Reference> refsPerAuthor = getRefsPerAttribute("author", refsPerYear.get(year), authorEquivalenceClasses);

            for (Sentence authorSentence : refsPerAuthor.keySet()) {
                List<Reference> refs = new LinkedList<Reference>(refsPerAuthor.get(authorSentence));

                allRefEquivalenceClasses.addAll(clusterer.findEquivalenceClasses(refs, journalVolPageMatcher));
            }

        }
        return allRefEquivalenceClasses;
    }

    /**
     * Creates a list of sentences. Each sentence is created from a set of Strings obtained
     * from a specified field of each object in a set
     * @param set The set of objects whose specified fields will be used to construct the list of sentences
     * @param field The field that will be used from each object in the {@code set} to create a sentence
     * @param separator The separator of the words in each sentence
     * @return The list of sentences, where each sentence is constructed from the string
     * obtained from the {@code field} of each object in {@code set}
     */
    public List<Sentence> getSentenceListFromField(Set<?> set, Field field, String separator) {
    	field.setAccessible(true);
        List<Sentence> sentences = new ArrayList<Sentence>();
        for (Object obj : set) {
            try {
                sentences.add(new Sentence(String.valueOf(field.get(obj)), separator));
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
        }
        return sentences;

    }

    public OrderedMultiMap<Sentence, Reference> getRefsPerAttribute(String fieldName, Set<Reference> refs, Collection<Set<Sentence>> equivalencyClasses) {
        Map<String, Set<Sentence>> stringToClusterMap = createClusterMap(equivalencyClasses);

        OrderedMultiMap<Sentence, Reference> refsPerAttribute = new OrderedMultiMap<Sentence, Reference>();
        for (Reference r : refs) {
            String value;
            try {
            	
                Field field = Reference.class.getDeclaredField(fieldName);
                field.setAccessible(true);
				value = (String) field.get(r);
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
            if (stringToClusterMap.get(value) != null) {
                refsPerAttribute.add(stringToClusterMap.get(value).iterator().next(), r);
            }
        }
        return refsPerAttribute;
    }

    private Map<String, Set<Sentence>> createClusterMap(Collection<Set<Sentence>> sentenceClusters) {
        Map<String, Set<Sentence>> clusterMap = new LinkedHashMap<String, Set<Sentence>>();
        for (Set<Sentence> cluster : sentenceClusters) {
            for (Sentence s : cluster) {
                clusterMap.put(s.string, cluster);
            }
        }
        return clusterMap;
    }

    public OrderedMultiMap<String, Reference> getRefsPerYear(Collection<Reference> refs) {
        OrderedMultiMap<String, Reference> refsPerYear = new OrderedMultiMap<String, Reference>();

        for (Reference r : refs) {
            if (r.getYear() != null) {
                refsPerYear.add(r.getYear(), r);
            }
        }
        return refsPerYear;
    }

    public Map<Reference, String> createReplacementsFromClusters(Collection<Set<Reference>> clusters) {
        LOGGER.info("Defining new names for references");

        Map<Reference, String> replacements = new LinkedHashMap<Reference, String>();
        for (Set<Reference> cluster : clusters) {
            Iterator<Reference> it = cluster.iterator();
            Reference superRef = rm.createSuperReference(cluster);
            while (it.hasNext()) {
                replacements.put(it.next(), superRef.originalString);
            }
        }
        LOGGER.info("DONE");

        return replacements;
    }

}
